<!--
 * @Author: xcl
 * @Date: 2022-10-11 09:48:49
 * @LastEditors: Please set LastEditors
 * @LastEditTime: 2022-10-12 17:30:10
 * @Description: [头部注释]
-->
<!--
 * @Author: xcl
 * @Date: 2022-04-20 18:05:17
 * @LastEditors: Please set LastEditors
 * @LastEditTime: 2022-10-11 16:27:33
 * @Description: BPMN 流程图 组件
-->
<template>
  <div class="bpmn-com">
    <!-- BPMN 流程图的容器元素 -->
    <div id="canvas" :class="[type]"></div>
    <!-- BPMN 侧边栏扩展、控制面板 -->
    <div id="properties" v-if="!type || type == 'Modeler'"></div>
    <!-- 操作按钮 -->
    <ul id="buttons">
      <li class="t">下载</li>
      <li>
        <a ref="saveDiagram" href="javascript:" title="下载BPMN图">BPMN图表</a>
      </li>
      <li>
        <a ref="saveSvg" href="javascript:" title="下载为SVG图像">SVG图像</a>
      </li>
      <li class="t">工具</li>
      <li>
        <p class="btn" @click="handlerUndo" title="撤销">撤销</p>
      </li>
      <li>
        <p class="btn" @click="handlerRedo" title="恢复">恢复</p>
      </li>
      <li>
        <p class="btn" @click="handlerZoom(0.1)" title="放大">放大</p>
      </li>
      <li>
        <p class="btn" @click="handlerZoom(-0.1)" title="缩小">缩小</p>
      </li>
      <li>
        <p class="btn" @click="handlerZoom(0)" title="还原">还原</p>
      </li>
      <li class="t">上传</li>
      <li>
        <el-upload
          action="#"
          :http-request="function () {}"
          :multiple="false"
          :limit="999"
          :show-file-list="false"
          :before-upload="beforeUpload"
          accept="*"
        >
          <p class="btn" title="导入BPMN图表">导入BPMN</p>
        </el-upload>
      </li>
    </ul>
  </div>
</template>

<script>
/** bpmn-js的相关的依赖 */
// 查看器 - Viewer
import Viewer from "bpmn-js/lib/Viewer"; // 或者 import Viewer from "bpmn-js"; 或者 import Viewer from "bpmn-js/dist/bpmn-viewer.development.js"; 或者 import Viewer from "bpmn-js/dist/bpmn-viewer.production.min.js";
// 浏览查看器 - NavigatedViewer
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer"; // 或者 import NavigatedViewer from "bpmn-js/dist/bpmn-navigated-viewer.development.js"; 或者 import NavigatedViewer from "bpmn-js/dist/bpmn-navigated-viewer.production.min.js";
// 建模器 - Modeler
import Modeler from "bpmn-js/lib/Modeler"; // 或者 import Modeler from "bpmn-js/dist/bpmn-modeler.development.js"; 或者 import Modeler from "bpmn-js/dist/bpmn-modeler.production.min.js";

// camunda 官方 - 侧边栏扩展、控制面板
import {
  BpmnPropertiesPanelModule, // 右边的工具栏
  BpmnPropertiesProviderModule, // 左边工具栏以及节点
} from "bpmn-js-properties-panel";

/** 汉化 */
// 国际化工具
import customTranslate from "./i18n/customTranslate";
// 自定义汉化模块
// 注：我们需要使用bpmn-js内部使用的数组语法，'value' 告诉 bmpn-js 使用该函数，而不是试图实例化它。
var customTranslateModule = {
  translate: ["value", customTranslate],
};

// bpmn模型描述符
import bpmnModdleDescriptor from "bpmn-moddle/resources/bpmn/json/bpmn.json";

/** 通用的用户界面附加功能 */
// 为 diagram-js 提供的小地图（鹰眼）
import MinimapModule from "diagram-js-minimap";
// 用于 diagram-js 的原点十字准线
import OriginModule from "diagram-js-origin";
export default {
  name: "BpmnCom",
  components: {},
  data() {
    return {
      BpmnJS: null,
      diagramXML: null,
      scale: 1,
    };
  },
  props: {
    /**
     * 创建的类型，有以下3个值可选，默认：Modeler
     * Viewer 查看器 （用作预览流程图，不可编辑、不可拖拽、不可缩放）
     * NavigatedViewer 浏览查看器 （用作预览流程图，不可编辑、支持缩放拖拽）
     * Modeler 建模器 （用作编辑、重绘、新建流程图，可编辑、可拖拽、可缩放）
     */
    type: {
      type: String,
      required: true,
    },
    /**
     * bpmn 的 xml 文档地址，默认：''
     * 用作预览流程图、编辑/修改流程图时
     */
    url: {
      type: String,
      default: "",
    },
    /**
     * 侧边栏扩展、控制面板，默认：true
     */
    properties: {
      type: Boolean,
      default: true,
    },
    /**
     * 小地图，默认：true
     */
    minimap: {
      type: Boolean,
      default: true,
    },
    /**
     * 原点十字准线，默认：true
     */
    origin: {
      type: Boolean,
      default: true,
    },
  },
  watch: {
    url: function (val) {
      this.$emit("loading");
      this.fetchDiagram(val);
    },
    diagramXML: function (val) {
      let self = this;
      // 导入流程图
      try {
        this.BpmnJS.importXML(val);
        // 导入成功...
        self.$emit("importSuccess");
      } catch (error) {
        // 导入失败...
        self.$emit("importFail", error);
      }
    },
  },
  created() {},
  mounted() {
    this.init();
  },
  beforeDestroy() {
    this.BpmnJS && this.BpmnJS.destroy();
  },
  destroyed() {},
  methods: {
    /**
     * 初始化流程设计器对象
     */
    init() {
      var self = this;

      // 配置项
      var _options = Object.assign(
        {
          container: "#canvas",
        },
        {
          // 设置 键盘事件绑定
          keyboard: {
            bindTo: window,
          },
          // 添加 控制板/工具栏
          propertiesPanel: {},
          // 添加 模型扩展 bpmn、activiti、camunda等
          moddleExtensions: {},
          // 添加 自定义模块
          additionalModules: [],
        }
      );
      if ((!this.type || this.type == "Modeler") && this.properties) {
        // 添加 侧边栏扩展、控制面板
        _options.propertiesPanel.parent = "#properties";
        _options.additionalModules.push(BpmnPropertiesPanelModule);
        _options.additionalModules.push(BpmnPropertiesProviderModule);
      }
      _options.additionalModules.push(customTranslateModule); // 汉化
      if (this.minimap) {
        // 添加 小地图
        _options.additionalModules.push(MinimapModule);
      }
      if (this.origin) {
        // 添加 原点十字准线
        _options.additionalModules.push(OriginModule);
      }
      _options.moddleExtensions.bpmn = bpmnModdleDescriptor; // bpmn模型描述符

      // 创建一个 NavigatedViewer / Modeler/ Viewer
      var BpmnJS;
      switch (this.type) {
        case "":
          BpmnJS = Modeler;
          break;
        case undefined:
          BpmnJS = Modeler;
          break;
        case "Modeler":
          BpmnJS = Modeler;
          break;
        case "NavigatedViewer":
          BpmnJS = NavigatedViewer;
          break;
        case "Viewer":
          BpmnJS = Viewer;
          break;
        default:
          break;
      }
      this.BpmnJS = new BpmnJS(_options);

      // 导入 import.done
      this.BpmnJS.on("import.done", function (event) {
        var error = event.error;
        var warnings = event.warnings;
        if (error) {
          self.$emit("error", error);
        } else {
          self.$emit("shown", warnings);
        }
        // 让图表能自适应屏幕
        self.BpmnJS.get("canvas").zoom("fit-viewport");
      });

      // 获取 BPMN 的 XML 文档
      if (this.url) {
        this.fetchDiagram(this.url);
      }

      // 下载画图
      this.downloadDraw();
    },

    /**
     * 下载画图
     */
    downloadDraw() {
      let self = this;
      // 1. 获取a标签dom节点
      const downloadLink = this.$refs.saveDiagram;
      const downloadSvgLink = this.$refs.saveSvg;
      // 2. 给图表绑定事件，当图表有发生改变就会触发这个事件
      this.BpmnJS.on("commandStack.changed", function () {
        self.saveSVG(function (err, svg) {
          self.setEncoded(downloadSvgLink, "diagram.svg", err ? null : svg);
        });

        self.saveDiagram(function (err, xml) {
          self.setEncoded(downloadLink, "diagram.bpmn", err ? null : xml);
        });
      });
    },

    /**
     * 获取 BPMN 的 XML 文档（字符串）
     * @param {*} url
     */
    fetchDiagram: function (url) {
      var self = this;
      fetch(url)
        .then(function (response) {
          return response.text();
        })
        .then(function (text) {
          self.diagramXML = text;
        })
        .catch(function (error) {
          self.$emit("error", error);
        });
    },

    /**
     * 下载为SVG格式,done是个函数，调用的时候传入的
     * @param {*} done
     */
    saveSVG(done) {
      // 把传入的done再传给bpmn原型的saveSVG函数调用
      this.BpmnJS.saveSVG(done);
    },
    /**
     * 下载为bpmn格式,done是个函数，调用的时候传入的
     * @param {*} done
     */
    saveDiagram(done) {
      // 把传入的done再传给bpmn原型的saveXML函数调用
      this.BpmnJS.saveXML({ format: true }, function (err, xml) {
        done(err, xml);
      });
    },
    /**
     * 当图发生改变的时候会调用这个函数，这个data就是图的xml
     * @param {*} link
     * @param {*} name
     * @param {*} data
     */
    setEncoded(link, name, data) {
      // 把xml转换为URI，下载要用到的
      const encodedData = encodeURIComponent(data);
      // 获取到图的xml，保存就是把这个xml提交给后台
      this.xmlStr = data;
      // 下载图的具体操作,改变a的属性，className令a标签可点击，href令能下载，download是下载的文件的名字
      if (data) {
        link.className = "active";
        link.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
        link.download = name ? name : this.getFilename(data);
      }
    },
    /**
     * 获取xml的名称
     * @param {*} xml 图的xml
     */
    getFilename(xml) {
      let start = xml.indexOf("process");
      let filename = xml.substr(start, xml.indexOf(">"));
      filename = filename.substr(filename.indexOf("id") + 4);
      filename = filename.substr(0, filename.indexOf('"'));
      return filename;
    },

    /**
     * 恢复
     */
    handlerRedo() {
      this.BpmnJS.get("commandStack").redo();
    },
    /**
     * 撤销
     */
    handlerUndo() {
      this.BpmnJS.get("commandStack").undo();
    },
    /**
     * 放大、缩小、还原
     * @param {*} radio
     */
    handlerZoom(radio) {
      const newScale = !radio ? 1.0 : this.scale + radio;
      this.BpmnJS.get("canvas").zoom(newScale);
      this.scale = newScale;
    },

    /**
     * 上传文件
     * @param {*} file
     */
    beforeUpload(file) {
      const isBPMN = file.name.includes(".bpmn");
      if (isBPMN) {
        this.openBpmn(file);
      } else {
        this.$message.warning("上传文件格式不对！");
        return false;
      }
    },
    /**
     * 打开并读取 bpmn 文件
     * @param {*} file
     */
    openBpmn(file) {
      const reader = new FileReader();
      // 读取File对象中的文本信息，编码格式为UTF-8
      reader.readAsText(file, "utf-8");
      reader.onload = () => {
        // 读取完毕后将文本信息导入到Bpmn建模器
        this.convertXmlStrToDiagram(reader.result);
      };
      return false;
    },
    /**
     * 将字符串 XmlStr 转换成图显示出来
     * @param {*} xmlStr BPMN流程XML报文
     */
    convertXmlStrToDiagram(xmlStr) {
      let self = this;
      this.BpmnJS.importXML(xmlStr, (err) => {
        if (err) {
          // 这里是失败之后的回调
          self.$message.error("打开模型出错，请确认该模型符合 BPMN2.0 规范");
        } else {
          // 这里是成功之后的回调
          console.log("成功导入模型");
        }
      });
    },
  },
};
</script>

<style scoped>
/** bpmn-js.css */
@import "./style/bpmn-js/bpmn-js.css";
/** 左边工具栏以及编辑节点的样式 */
@import "./style/bpmn-js/diagram-js.css";
@import "./style/bpmn-js/bpmn-font/css/bpmn.css";
@import "./style/bpmn-js/bpmn-font/css/bpmn-codes.css";
@import "./style/bpmn-js/bpmn-font/css/bpmn-embedded.css";
/** 侧边栏扩展、控制面板的样式 */
@import "./style/bpmn-js-properties-panel/properties-panel.css";
@import "./style/bpmn-js-properties-panel/element-templates.css";
/** 小地图的样式 */
@import "./style/diagram-js-minimap/diagram-js-minimap.css";
</style>

<style lang="less" scoped>
.bpmn-com {
  width: 100%;
  height: 100%;
  position: relative;
  #canvas {
    width: 100%;
    height: 100%;
    // 去掉 bpmn.io
    /deep/.bjs-powered-by {
      display: none !important;
    }
    // 更改小地图位置
    /deep/.djs-minimap {
      z-index: 99;
      right: 20px;
      top: 3%;
      z-index: 99;
      right: 225px;
      top: 3%;
    }
  }
  #canvas.NavigatedViewer,
  #canvas.Viewer {
    // 更改小地图位置
    /deep/.djs-minimap {
      z-index: 99;
      right: 20px;
      top: 3%;
    }
  }
  #properties {
    position: absolute;
    top: 3%;
    right: 20px;
    width: 200px;
    border: 1px solid #b9bcc6;
    height: 94%;
    background: #fff;
    border-radius: 2px;
    box-sizing: border-box;
    overflow: auto;
  }
  #buttons {
    position: absolute;
    left: calc(20px + 0px);
    bottom: 3%;
    background: #f7f7f8;
    border: 1px solid #b9bcc6;
    width: calc(100% - 20px - 20px - 200px - 2px);
    box-sizing: border-box;
    height: 46px;
    display: flex;
    align-items: center;
    .t {
      color: #000;
    }
    & > li {
      margin: 0 5px;
      & > a {
        color: #999;
        background: #fff;
        cursor: not-allowed;
        padding: 3px 6px;
        border: 1px solid #b9bcc6;
        text-decoration: none;
        box-sizing: border-box;
        border-radius: 3px;
        font-size: 14px;
        &.active {
          color: #333;
          background: #fff;
          cursor: pointer;
        }
      }
      .btn {
        color: #333;
        background: #fff;
        cursor: pointer;
        padding: 3px 6px;
        border: 1px solid #b9bcc6;
        text-decoration: none;
        box-sizing: border-box;
        border-radius: 3px;
        font-size: 14px;
      }
    }
  }
}
</style>
